package fr.cephb.doipdf;

import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.XMLEvent;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;


public class Doi2PdfResolver
	{
	private static final String DOI_ORG="http://dx.doi.org";
	private interface URIHandler
		{
		public boolean isA(String uri);
		public String getName();
		public String resolve(String uri) throws IOException;
		}
	
	/**
	 * DOIHandler
	 */
	private class DOIHandler
		implements URIHandler
		{
		private Pattern regex;
		DOIHandler()
			{
			this.regex=Pattern.compile(
				"(doi:)?\\s?(10\\.\\d{4}\\/\\S+)",
				Pattern.CASE_INSENSITIVE);
			}
		
		@Override
		public String getName()
			{
			return "doi";
			}
		
		@Override
		public boolean isA(String uri)
			{
			return regex.matcher(uri).matches();
			}
		@Override
		public String resolve(String doi) throws IOException
			{
			HttpClient httpClient= getClient();
			HttpMethodBase post= null;
			String pdf=null;
			String url=DOI_ORG;
			try
				{
				if(!doi.toLowerCase().startsWith("doi:"))
					{
					doi="doi:"+doi;
					}
				doi=doi.replaceAll("doi:\\s?", "doi:");
				LOG.info("post to "+DOI_ORG);
				post=new PostMethod(DOI_ORG);
				post.setFollowRedirects(false);
				PostMethod.class.cast(post).addParameter("hdl",doi);
				int depth=0;
				while(depth<10)
					{
					LOG.info("execute "+url+" ");
					LOG.info("getFollowRedirects:"+post.getFollowRedirects()+" "+post.getClass());
					int status = httpClient.executeMethod(post);
					if(status!=HttpStatus.SC_OK &&
					   status!=HttpStatus.SC_SEE_OTHER &&
					   status!=HttpStatus.SC_MOVED_TEMPORARILY &&
					   status!=HttpStatus.SC_MOVED_PERMANENTLY &&
					   status!=HttpStatus.SC_TEMPORARY_REDIRECT
						)
						{
						throw new IOException("connection failure for "+url+" status:"+status);
						}
					
					
					Header header=post.getResponseHeader("Location");
					if(header==null )
						{
						LOG.info("no Location header in "+url+" "+post.getURI());
						
						if(depth==0)
							throw new IOException(
							"Expected a redirect for "+url
							);
						pdf=null;
						break;
						}
					
					String documenturl=header.getValue();
					LOG.info("document has been redirected to "+documenturl);
					if(!documenturl.startsWith("http"))
						{
						documenturl=new java.net.URL(new java.net.URL(url),documenturl).toString();
						}
					pdf= Doi2PdfResolver.this.resolve(documenturl);
					post.releaseConnection();
					post=null;
					if(pdf!=null) break;
					++depth;
					LOG.info("redirect to "+documenturl);
					url=documenturl;
					
					post= new GetMethod(documenturl);
					post.setFollowRedirects(false);
					}
				if(pdf==null)
					{
					throw new IOException("Cannot resolve doi:"+doi);
					}
				return pdf;
				}
			finally
				{
				if(post!=null) post.releaseConnection();
				}
			}
		}
	
	/**
	 * 
	 * NatureHandler
	 *
	 */
	private class NatureHandler
	implements URIHandler
		{
		/*
		 * http://www.nature.com/nature/journal/v461/n7267/full/4611060a.html
		 * http://www.nature.com/nature/journal/v461/n7267/pdf/4611060a.pdf
		 * http://www.nature.com/news/2009/091021/full/4611032a.html
		 * http://www.nature.com/news/2009/091021/pdf/4611032a.pdf
		 * 
		 * http://www.nature.com/ng/journal/v41/n10/abs/ng.437.html
		 * http://www.nature.com/ng/journal/v41/n10/pdf/ng.437.pdf
		 */
		private Pattern regex;
		NatureHandler()
			{
			this.regex=Pattern.compile(
				"(http://www.nature.com/(\\w+/){1,})([a-z]+)/([a-z_\\.A-Z0-9]+)\\.html",
				Pattern.CASE_INSENSITIVE);
			}
		
		@Override
		public String getName()
			{
			return "Nature";
			}
		
		@Override
		public boolean isA(String uri)
			{
			return regex.matcher(uri).matches();
			}
		@Override
		public String resolve(String url) throws IOException
			{
			Matcher matcher= this.regex.matcher(url);
			matcher.matches();//compute groups
			return matcher.group(1)+"pdf/"+matcher.group(4)+".pdf";
			}
		}
	
	/**
	 * 
	 * Generic
	 *
	 */
	private class GenericHandler
	implements URIHandler
		{
		/*
		 * http://www.jbc.org/content/284/44/29997
		 * http://www.jbc.org/content/284/44/29997.full.pdf
		 * 
		 */
		private Pattern regex;
		GenericHandler()
			{
			this.regex=Pattern.compile(
				"(http://www\\.\\w+\\.\\w+/content/[/0-9]+)(\\.(full|long)(\\+html)?)?",
				Pattern.CASE_INSENSITIVE);
			}
		
		@Override
		public String getName()
			{
			return "GENERIC";
			}
		
		@Override
		public boolean isA(String uri)
			{
			return regex.matcher(uri).matches();
			}
		@Override
		public String resolve(String url) throws IOException
			{
			Matcher matcher= this.regex.matcher(url);
			matcher.matches();//compute groups
			return matcher.group(1)+".full.pdf";
			}
		}
	
	
	
	/**
	 * PMIDHandler
	 *
	 */
	private class PMIDHandler
		implements URIHandler
		{
		private Pattern regex1=Pattern.compile(
			"http://www.ncbi.nlm.nih.gov/pubmed/(\\d+).*");
		private Pattern regex2=Pattern.compile("((pubmed|pmid):)?(\\d+)");
		@Override
		public String getName()
			{
			return "PUBMED";
			}
		@Override
		public boolean isA(String uri)
			{
			return	regex1.matcher(uri).matches() ||
					regex2.matcher(uri).matches();
			}
		
		@Override
		public String resolve(String uri) throws IOException
			{
			String pmid=null;
			Matcher match=regex1.matcher(uri);
			if(match.matches())
				{
				pmid=match.group(1);
				}
			else
				{
				match=regex2.matcher(uri);
				match.matches();
				pmid=match.group(3);
				}
			
			LOG.info("pmid="+pmid);
			
			XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
			xmlInputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
			xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
			xmlInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE);
			HttpClient httpClient= getClient();
			GetMethod method= null;
			try
				{
				String efetchurl="http://www.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id="+
						pmid+
						"&retmode=xml&rettype=abstract";
				
				method=new GetMethod(efetchurl);
				int status = httpClient.executeMethod(method);
				if(status!=200)
					{
					throw new IOException("cannot get "+efetchurl);
					}
				
				String doi=null;
				InputStream in=method.getResponseBodyAsStream();
				XMLEventReader reader= xmlInputFactory.createXMLEventReader(
					in
					);
				final QName IdType=new QName("IdType"); 
				while(reader.hasNext())
					{
					XMLEvent evt=reader.nextEvent();
					if(!evt.isStartElement()) continue;
					String name=evt.asStartElement().getName().getLocalPart();
					if(!name.equals("ArticleId")) continue;
					Attribute att=evt.asStartElement().getAttributeByName(IdType);
					if(att==null ) continue;
					String type=att.getValue();
					if(type.equals("doi"))
						{
						doi= reader.getElementText().trim();
						break;
						}
					}
				reader.close();
				if(doi==null)
					{
					return null;
					}
				DOIHandler h2=  new DOIHandler();
				if(!h2.isA(doi)) throw new IOException("Not a doi: "+doi);
				return h2.resolve(doi);
				}
			catch(XMLStreamException err)
				{
				throw new IOException(err);
				}
			finally
				{
				if(method!=null) method.releaseConnection();
				}
			}
		}
	
	private class OxfordHandler
		implements URIHandler
		{
		/* http://bioinformatics.oxfordjournals.org/cgi/content/abstract/25/21/2735
		 * http://bioinformatics.oxfordjournals.org/cgi/content/full/25/21/2735
		 * http://bioinformatics.oxfordjournals.org/cgi/reprint/25/21/2735.pdf
		 */
		private Pattern regex=Pattern.compile("(http://\\w+.oxfordjournals.org/cgi)/content/(abstract|full)((/\\d+){2,})");
		@Override
		public String getName()
			{
			return "OXFORD";
			}
		@Override
		public boolean isA(String uri)
			{
			return regex.matcher(uri).matches();
			}
		@Override
		public String resolve(String uri) throws IOException
			{
			Matcher m=this.regex.matcher(uri);
			m.matches();//calcul groups
			return m.group(1)+"/reprint"+m.group(3)+".pdf";
			}
		}
	
	private class CSHLHandler
	implements URIHandler
		{
		/* 
		 * http://rnajournal.cshlp.org/content/15/11/2035
		 * http://rnajournal.cshlp.org/content/15/11/2035.full
		 * http://rnajournal.cshlp.org/content/15/11/2035.full.pdf
		 */
		private Pattern regex=Pattern.compile(
			"(http://\\w+.cshlp.org/content((/\\d+){2,}))(\\.full)?");
		@Override
		public String getName()
			{
			return "CSHL";
			}
		@Override
		public boolean isA(String uri)
			{
			return regex.matcher(uri).matches();
			}
		@Override
		public String resolve(String uri) throws IOException
			{
			Matcher m=this.regex.matcher(uri);
			m.matches();//calcul groups
			return m.group(1)+".pdf";
			}
		}
	
	/**
	 * SpringHandler
	 *
	 */
	private class SpringerHandler
	implements URIHandler
		{
		/* 
		 * 
		 * http://www.springerlink.com/index/10.1007/s00453-007-9157-8
		 * http://www.springerlink.com/content/c104731297q64224/
		 * http://www.springerlink.com/content/c104731297q64224/fulltext.pdf
		 */
		private Pattern regex=Pattern.compile(
			"(http://www.springerlink.com/content/\\w+)/?");
		@Override
		public String getName()
			{
			return "SPRINGER";
			}
		@Override
		public boolean isA(String uri)
			{
			return regex.matcher(uri).matches();
			}
		@Override
		public String resolve(String uri) throws IOException
			{
			Matcher m=this.regex.matcher(uri);
			m.matches();//calcul groups
			return m.group(1)+"/fulltext.pdf";
			}
		}
	
	private class BMCHandler
		implements URIHandler
		{
		/* e.g.
		 * http://www.biomedcentral.com/1471-2105/10/352/
		 * http://www.biomedcentral.com/content/pdf/1471-2105-10-352.pdf
		 */
		private Pattern regex =Pattern.compile("http://www.biomedcentral.com/([0-9\\-]+/[0-9]+/[0-9]+)(/abstract)?"); 
		@Override
		public String getName()
			{
			return "BMC";
			}
		@Override
		public boolean isA(String uri)
			{
			return regex.matcher(uri).matches();
			}
		@Override
		public String resolve(String uri) throws IOException
			{
			Matcher matcher=regex.matcher(uri);
			matcher.matches();
			return "http://www.biomedcentral.com/content/pdf/"+
					matcher.group(1).replace('/', '-')+
					".pdf"
					;
			}
		}
	
	private class MITPressHandler
		implements URIHandler
		{
		/*
		 * http://www.mitpressjournals.org/doi/abs/10.1162/089976606774841558
		 * http://www.mitpressjournals.org/doi/pdf/10.1162/089976606774841558
		 */
		private Pattern regex=Pattern.compile("http://www.mitpressjournals.org/doi/abs/([a-zA-Z_0-9\\./]+)");
		@Override
		public String getName()
			{
			return "MITPress";
			}
		@Override
		public boolean isA(String uri)
			{
			return regex.matcher(uri).matches();
			}
		@Override
		public String resolve(String uri) throws IOException
			{
			Matcher m=this.regex.matcher(uri);
			m.matches();
			return "http://www.mitpressjournals.org/doi/pdf/"+m.group(1);
			}
		}
	
	private static final Logger LOG= Logger.getLogger("fr.cephb");
	/** http client **/
	private HttpClient client;		
	/** handlers */
	private URIHandler handlers[]=null;
	
	public Doi2PdfResolver()
		{
		this.client=new HttpClient();
		this.client.getParams().setConnectionManagerTimeout(30*1000L);
		this.client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
		
		this.handlers= new URIHandler[]{
			new DOIHandler(),
			new NatureHandler(),
			new GenericHandler(),
			new OxfordHandler(),
			new CSHLHandler(),
			new SpringerHandler(),
			new BMCHandler(),
			new PMIDHandler(),
			new MITPressHandler()
			};
		
		}
	
	public String resolve(String uri) throws IOException
		{
		LOG.info("resolving "+uri);
		if(uri==null) throw new NullPointerException("uri is null");
		uri=uri.trim();
		if(uri.trim().length()==0) throw new IOException("uri is empty");
		for(URIHandler handler: this.handlers)
			{
			if(handler.isA(uri))
				{
				LOG.info(uri+" is a "+handler.getName());
				return handler.resolve(uri);
				}
			else
				{
				LOG.info(uri+" is NOT a "+handler.getName());
				}
			}
		LOG.info("cannot resolve "+uri);
		return null;
		}
	
	public HttpClient getClient()
		{
		return client;
		}
		public static void main(String[] args)
			{
			Doi2PdfResolver app= new Doi2PdfResolver();
			try
				{
				int optind=0;
				while(optind< args.length)
					{
					if(args[optind].equals("-h") ||
					   args[optind].equals("-help") ||
					   args[optind].equals("--help"))
						{
						System.err.println("Options:");
						System.err.println(" -h help; This screen.");
						return;
						}
					else if(args[optind].equals("-t"))
						{
						app.getClient().getParams().setSoTimeout(Integer.parseInt(args[++optind]));
						}
					else if(args[optind].equals("--"))
						{
						optind++;
						break;
						}
					else if(args[optind].startsWith("-"))
						{
						System.err.println("Unknown option "+args[optind]);
						return;
						}
					else 
						{
						break;
						}
					++optind;
					}
				while(optind<args.length)
					{
					try
						{
						String uri=args[optind++];
						System.out.println(uri+"\t"+app.resolve(uri));
						}
					catch (IOException e)
						{
						System.out.println("#"+e.getMessage());
						}
					}
				} 
			catch(Throwable err)
				{
				err.printStackTrace();
				}
		}
	
	}
